package com.fitso.model.bean.measure;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

import org.apache.commons.lang.builder.CompareToBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

import com.fitso.model.util.common.ReflectionUtils;

/**
 * <p>
 * Represents a measurement such as foot, inch, cm, meter, dollar, euro. This
 * class does conversions of one unit to another by translating the foreign unit
 * into the instance unit by getting the base value. The base value is
 * determined by the implementations; for example, length units might set the
 * base to mm, or can be set uniquely such as conversion rates for money. This
 * class also provides some utility methods for adding, subtracting, multiplying
 * and dividing units. Notice that this class does a best effort calculation and
 * does not make any assumptions such as if a conversion is possible - this only
 * makes sense since you can do things like Mass / Length to give a mass to
 * length ratio but Mass * Currency doesn't necessarily make any sense. This
 * class holds higher accuracy when doing modifications that might alter the
 * scale: multiply, divide, convert; but degrades the accuracy when
 * determining equality.
 * </p>
 * <p>
 * This class is only for use within fitso. It is not intended for highly
 * accurate calculations. Once the calculations start getting very large or very
 * small the results can no longer be trusted.
 * </p>
 * 
 * @author timothystorm
 */
public abstract class Measure implements Comparable<Measure>, Serializable {
	// Only change on breaking changes, i.e., removing fields
	private static final long serialVersionUID = 1L;
	
	private static final Double CONVERSION_DELTA = 0.0001;

	private static final MathContext LENIENT = new MathContext(7, RoundingMode.HALF_UP);
	
	private static final MathContext STRICT = new MathContext(16, RoundingMode.HALF_UP);

	public static Measure clone(Measure measure) {
		Measure m = (Measure) ReflectionUtils.instance(measure.getClass());
		m._value = measure._value;
		return m;
	}

	private BigDecimal _value;

	public Measure() {}

	public Measure(BigDecimal value) {
		_value = value;
	}

	public Measure(Double value) {
		this(BigDecimal.valueOf(value));
	}

	public Measure(Integer value) {
		this(BigDecimal.valueOf(value));
	}

	/**
	 * Conversion constructor. Takes in the foreign unit and converts it to the
	 * instance unit
	 * 
	 * @param measure
	 */
	public Measure(Measure measure) {
		convert(measure);
	}

	public Measure add(BigDecimal value) {
		_value = _value.add(value);
		return this;
	}

	/**
	 * Adds the value to this unit, it is assumed that the parameter is of the
	 * same unit. Suitable for mixed unit calculations.
	 * 
	 * @param value
	 * @return self instance with the arithmetic applied
	 */
	public Measure add(Double value) {
		return add(BigDecimal.valueOf(value));
	}

	public Measure add(Integer value) {
		return add(BigDecimal.valueOf(value));
	}

	/**
	 * Adds the unit to this unit by first converting the foreign unit to the
	 * instance unit. Not intended for mixed unit calculations, use
	 * {@link #add(Double)} instead.
	 * 
	 * @param value
	 * @return self instance with the arithmetic applied
	 * @see {@link #add(Double)}
	 */
	public Measure add(Measure measure) {
		if (measure != null) {
			if (_value == null) {
				_value = copy(measure._value);
			} else {
				if (isSameMeasure(measure)) {
					if (isSameClass(measure)) {
						_value = _value.add(measure._value);
					} else {
						_value = _value.add(convert(measure)._value);
					}
				} else {
					_value = _value.add(measure._value);
				}
			}
		}
		return this;
	}

	public Measure clone() {
		return clone(this);
	}

	public int compareTo(Measure other) {
		return new CompareToBuilder().append(toBase(), other.toBase()).append(getUnitMark(), other.getUnitMark())
				.toComparison();
	}

	/**
	 * converts the foreign measure to this measure
	 * 
	 * @param measure
	 * @return
	 */
	protected Measure convert(Measure measure) {
		if (measure != null) {
			if (isSameClass(measure)) {
				_value = copy(measure._value).round(STRICT);
			} else {
				_value = measure._value.multiply(measure.getToBaseUnitFactor()).multiply(getFromBaseUnitFactor());
				_value = _value.round(STRICT);
			}
		}
		return this;
	}

	private BigDecimal copy(BigDecimal value) {
		return new BigDecimal(value.toPlainString());
	}

	public Measure divide(BigDecimal value) {
		_value = _value.divide(value, STRICT);
		return this;
	}

	/**
	 * Divides the value from this value, it is assumed that the parameter is of
	 * the same unit. Suitable for mixed unit calculations.
	 * 
	 * @param value
	 * @return self instance with the arithmetic applied
	 */
	public Measure divide(Double value) {
		return divide(BigDecimal.valueOf(value));
	}

	public Measure divide(Integer value) {
		return divide(BigDecimal.valueOf(value));
	}

	/**
	 * Divides the unit from this value by first converting the foreign unit to
	 * the instance unit. Not intended for mixed unit calculations, use
	 * {@link #divide(Double)} instead.
	 * 
	 * @param value
	 * @return self instance with the arithmetic applied
	 * @see {@link #divide(Double)}
	 */
	public Measure divide(Measure measure) {
		if (measure != null) {
			if (isSameMeasure(measure)) {
				if (isSameClass(measure)) {
					_value = _value.divide(measure._value, STRICT);
				} else {
					_value = _value.divide(convert(measure)._value, STRICT);
				}
			} else {
				_value = _value.divide(measure._value, STRICT);
			}
		}
		return this;
	}

	public Double doubleValue() {
		return _value.doubleValue();
	}

	/**
	 * Determines if the object is equal to this {@link Measure}. So 1000g =
	 * 1000g and 1000g = 1kg. It also does a best effort on different unit
	 * scales so 10g = 0.35273oz.
	 * 
	 * @see {@link #equivalent(Object)}
	 */
	public boolean equals(final Object o) {
		if (o == null) {
			return false;
		}
		if (o == this) {
			return true;
		}

		Measure other = (Measure) o;

		if (isSameMeasure(other)) {
			if (isSameClass(other)) {
				// test for exact equality
				return _value.round(LENIENT).compareTo(other._value.round(LENIENT)) == 0;
			}
		}

		// test for similarity withing delta
		BigDecimal left = toBase();
		BigDecimal right = other.toBase();
		Double diff = left.subtract(right).abs().doubleValue();
		return diff <= CONVERSION_DELTA;
	}

	/**
	 * factor to convert from base unit
	 * 
	 * @return
	 */
	protected abstract BigDecimal getFromBaseUnitFactor();

	protected abstract Class<?> getMeasure();

	/**
	 * factor to convert to base unit
	 * 
	 * @return
	 */
	protected abstract BigDecimal getToBaseUnitFactor();

	/**
	 * 
	 * @return
	 */
	public abstract String getUnitMark();

	public BigDecimal getValue() {
		return _value;
	}

	public int hashCode() {
		return new HashCodeBuilder().append(toBase()).toHashCode();
	}

	/**
	 * @return is the underlying unit value initialized
	 */
	public boolean isInitialized() {
		return _value != null;
	}

	/**
	 * Utility method to determine if the provided unit is of the same instance
	 * as this unit. This is used to determine the type of calculation to
	 * perform.
	 * 
	 * @param measure
	 * @return
	 */
	private boolean isSameClass(Measure measure) {
		return measure == null ? false : this.getClass().equals(measure.getClass());
	}

	private boolean isSameMeasure(Measure measure) {
		return getMeasure().equals(measure.getMeasure());
	}

	public Measure multiply(BigDecimal value) {
		_value = _value.multiply(value, STRICT);
		return this;
	}

	/**
	 * Multiplies the value from this value, it is assumed that the parameter is
	 * of the same unit. Suitable for mixed unit calculations.
	 * 
	 * @param value
	 * @return self instance with the arithmetic applied
	 */
	public Measure multiply(Double value) {
		return multiply(BigDecimal.valueOf(value));
	}

	public Measure multiply(Integer value) {
		return multiply(BigDecimal.valueOf(value));
	}

	/**
	 * Multiplies the unit from this value by first converting the foreign unit
	 * to the instance unit. Not suitable for mixed unit calculations, use
	 * {@Link #multiply(Double)} instead.
	 * 
	 * @param value
	 * @return self instance with the arithmetic applied
	 * @see {@link #multiply(Double)}
	 */
	public Measure multiply(Measure measure) {
		if (measure != null) {
			if (isSameMeasure(measure)) {
				if (isSameClass(measure)) {
					_value = _value.multiply(measure._value, STRICT);
				} else {
					_value = _value.multiply(convert(measure)._value, STRICT);
				}
			} else {
				_value = _value.multiply(measure._value, STRICT);
			}
		}
		return this;
	}

	public Measure pow(int n) {
		_value = _value.pow(n);
		return this;
	}

	public void setValue(BigDecimal value) {
		_value = value;
	}

	public Measure subtract(BigDecimal value) {
		_value = _value.subtract(value);
		return this;
	}

	/**
	 * Subtracts the value from this value, it is assumed that the parameter is
	 * of the same unit. Suitable for mixed unit calculations.
	 * 
	 * @param value
	 * @return self instance with the arithmetic applied
	 */
	public Measure subtract(Double value) {
		return subtract(BigDecimal.valueOf(value));
	}

	public Measure subtract(Integer value) {
		return subtract(BigDecimal.valueOf(value));
	}

	/**
	 * Subtracts the unit from this value by first converting the foreign unit
	 * to the instance unit. Not intended for mix unit calculations, use
	 * {@link #subtract(Double)} instead.
	 * 
	 * @param value
	 * @return self instance with the arithmetic applied
	 * @see {@link #subtract(Double)}
	 */
	public Measure subtract(Measure measure) {
		if (measure != null) {
			if (_value == null) {
				_value = measure._value.negate();
			} else {
				if (isSameMeasure(measure)) {
					if (isSameClass(measure)) {
						_value = _value.subtract(measure._value);
					} else {
						_value = _value.subtract(convert(measure)._value);
					}
				} else {
					_value = _value.subtract(measure._value);
				}
			}
		}
		return this;
	}

	/**
	 * Utility method for getting this instance's value converted to the base
	 * value. The base value is determined by the implementations.
	 * 
	 * @return
	 */
	private BigDecimal toBase() {
		return _value.multiply(getToBaseUnitFactor()).round(STRICT);
	}

	public String toString() {
		return _value == null ? "measure not initialized" : (_value + " " + getUnitMark());
	}
}
